Skip to content

[cherry-pick] 마이페이지 기능 구현 및 디자인 개선#693

Merged
HA-SEUNG-JEONG merged 24 commits into
mainfrom
cherry/fix-myPage-to-main
May 25, 2026
Merged

[cherry-pick] 마이페이지 기능 구현 및 디자인 개선#693
HA-SEUNG-JEONG merged 24 commits into
mainfrom
cherry/fix-myPage-to-main

Conversation

@HA-SEUNG-JEONG
Copy link
Copy Markdown
Contributor

@HA-SEUNG-JEONG HA-SEUNG-JEONG commented May 25, 2026

개요

마이페이지 관련 미구현 기능(내가 작성한 글, 1:1 문의 페이지)을 신규 구현하고, 마이클래스 알림 토글 ON/OFF 양방향 제어를 추가했습니다. 탈퇴 모달 및 전역 토큰(primary-500 → 시스템 토큰) 불일치를 Figma 기준으로 수정하고, 임시저장 draft 중복 생성 방지 및 DRAFT→PUBLISHED 전환 처리를 개선했습니다.

원본 PR

Cherry-pick 대상 커밋 (23개)

  • 0faead4d — chore(api): v6 마이페이지 전용 axiosInstanceV6 추가
  • 1dab1a13 — feat(my-inquiry): 1:1 문의 API 훅 및 Zod 스키마 추가
  • bc196812 — feat(my-page): 마이페이지 내가 작성한 글 / 1:1 문의 페이지 구현
  • de057ddb — 마이 클래스 토글 비활성화 기능 추가 — localStorage 기반 클라이언트 상태 관리
  • e756be3d — NotificationSettingResponse dead field 제거 (empty, skipped)
  • c10631fb — 마이클래스 버튼 토큰 수정 및 코드 정리
  • d4e14ded — feat: 마이페이지 배너 UI 업데이트 및 토큰 수정
  • d88f208d — fix: 마이페이지 전역 primary-500 미존재 토큰 교체
  • 8aa20776 — chore: 마이페이지 에셋 추가
  • b0b76f53 — fix: 탈퇴 확인 모달 Figma 디자인 반영
  • 546c3f05 — chore(types): 클래스 결제 환불/취소 관련 타입 추가
  • c558d1d0 — chore(api): 클래스 결제 상세 조회 및 환불 요청 훅 추가
  • f69a9abd — feat(결제관리): 클래스 결제 취소·환불 플로우 분리 및 가상계좌 상세 조회
  • 3f52f326 — feat(마이피드): 임시 저장 피드 목록 UI 추가
  • e8031bec — chore(types): 빌더 피드 관리 응답 타입 추가
  • 29cafd3a — fix: 문의 첨부 미연동 숨김, 알림 토글 hydration 및 에러 처리 개선
  • 33af944e — chore(types): MyDraftBuilderFeedItemResponse 미사용 타입 제거
  • 8d7bc206 — refactor(api): useGetMyDraftBuilderFeeds 중복 훅 제거
  • 0db37fcc — fix(임시저장): draft 중복 생성 방지 및 DRAFT→PUBLISHED 전환 처리
  • cca0ecfc — fix(피드 등록): 임시저장 발행 후 피드 상세 페이지로 이동
  • f948eed1 — 테스트 코드 수정
  • 3b836477 — fix(ci): 스테이징 서버 다운 시 E2E 스킵으로 CI 보호
  • 06afccf0 — fix(ci): non-@auth E2E를 로컬 서버 대상으로 실행해 스테이징 의존성 제거

변경 파일

  • .github/workflows/ci.yml — E2E CI 개선 (스테이징 다운 시 스킵, 로컬 서버 대상 non-@auth 실행)
  • e2e/class/builder-feed.spec.ts — 테스트 코드 수정 (networkidle → waitForResponse)
  • public/my-page/discord-icon.png, feed-icon.svg — 마이페이지 에셋 추가
  • src/api/client/axios.ts — axiosInstanceV6 추가
  • src/app/(landing)/class/[slug]/(learning)/feed/write/page.tsx — 임시저장 draft 중복 방지, PUBLISHED 전환
  • src/app/(landing)/class/[slug]/(learning)/qa/write/page.tsx — draft 정리 추가
  • src/app/(service)/(my)/class-payment-management/page.tsx — 취소·환불 플로우 분리
  • src/app/(service)/(my)/my-class/ — 알림 토글 비활성화 모달 추가
  • src/app/(service)/(my)/my-inquiry/ — 1:1 문의 목록·작성 페이지 구현
  • src/app/(service)/(my)/my-page/ — 배너 UI, 탈퇴 모달 Figma 반영
  • src/app/(service)/(my)/my-posts/page.tsx — 내가 작성한 글 + 임시저장 목록 구현
  • src/components/payment/modals/class-refund-request-modal.tsx — 신규 환불 모달
  • src/hooks/queries/course/course-api.ts — 결제 상세·환불·피드 관리 훅 추가
  • src/hooks/queries/my-inquiry/inquiry-api.ts — 1:1 문의 API 훅 추가
  • src/types/api/course.types.ts — 결제·피드 관련 타입 추가
  • src/types/schemas/inquiry.schema.ts — 문의 Zod 스키마 추가

혼입 검증 결과

fix/myPage 브랜치에는 deploy-prod.yml, docs, 다른 E2E 파일 등 PR #690 범위 외 파일이 포함되어 있었으나, cherry-pick은 PR에 명시된 23개 커밋만 대상으로 진행했습니다.

Test plan

  • 마이페이지 > 내가 작성한 글 탭 — 게시물 목록 정상 조회
  • 마이페이지 > 1:1 문의 탭 — 문의 목록 정상 조회 및 작성 후 목록 반영
  • 마이클래스 알림 토글 ON → OFF 클릭 시 비활성화 모달 노출 및 해제 확인
  • 마이클래스 알림 토글 OFF → ON 클릭 시 알림 설정 모달 정상 진입
  • 탈퇴 모달 폰트 크기·색상 Figma와 일치 확인
  • 피드 임시저장 후 등록 시 피드 상세 페이지로 이동 확인
  • 클래스 결제 취소·환불 플로우 정상 동작 확인

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • 피드·Q&A 작성에 임시저장 기능 추가
    • 1:1 문의 작성·관리 기능 도입(임시저장 포함)
    • 결제 환불 요청 기능 및 단계형 환불 모달 추가
    • 결제 관리에서 가상계좌 조회 기능 제공
    • 알림 끄기 확인 모달 추가
  • Improvements

    • 알림 UI/토글 개선 및 로컬 비활성화 지원
    • 마이페이지·프로필·사이드바 라벨/스타일 개선
    • 빌더 피드 관리 화면·게시물 목록 개선
    • E2E 테스트 및 전반적 안정성 향상

Review Change Stack

HA-SEUNG-JEONG and others added 22 commits May 25, 2026 09:28
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… API 훅 및 Zod 스키마 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…작성한 글 / 1:1 문의 페이지 구현

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ocalStorage 기반 클라이언트 상태 관리

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
primary-500 미존재 토큰을 fill-brand-default-default 등으로 교체,
GiftEmailCard 제거, disabled 버튼 스타일 수정

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
디스코드/피드 배너 레이아웃 개선, primary-500 미존재 토큰을
text-text-brand/border-border-brand으로 교체, 사이드바 라벨 변경

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 토큰 교체

bg/text/border-primary-500, bg-brand-primary-500을 프로젝트 토큰
(fill-brand-default-default, text-brand, border-brand, rose-500 등)으로 일괄 교체

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
discord-icon.png, feed-icon.svg 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…취소 관련 타입 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 및 환불 요청 훅 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 플로우 분리 및 가상계좌 상세 조회

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…UI 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…답 타입 추가

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ation 및 에러 처리 개선

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lderFeedItemResponse 미사용 타입 제거

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…raftBuilderFeeds 중복 훅 제거

useGetMyBuilderFeedManagement({ status: 'DRAFT' })로 통합

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- feed/write: draftFeedIdRef로 feedId 추적, 재저장 시 updateFeed 사용
- feed/write: 등록하기 시 기존 draft는 updateFeed(PUBLISHED)로 전환
- feed/write: edit mode 수정하기에 status: PUBLISHED 추가
- qa/write: 등록 성공 시 localStorage draft 정리 추가
- my-posts: useGetMyBuilderFeedManagement({ status: DRAFT })로 교체

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 상세 페이지로 이동

임시저장 글 등록 시 피드 목록이 아닌 발행된 피드 상세로 이동.
신규 등록도 createFeed 응답의 feedId로 상세 페이지 이동.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…E 스킵으로 CI 보호

test.zeroone.it.kr DNS 불통 상태에서 전체 E2E 테스트가
net::ERR_NAME_NOT_RESOLVED로 실패하는 문제 해결.
커넥티비티 프리플라이트 추가 — 응답 없으면 STAGING_DOWN=true
설정 후 E2E 스텝 전체 스킵, 경고 메시지 출력.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…로컬 서버 대상으로 실행해 스테이징 의존성 제거

이전 bandaid fix(스테이징 다운 시 전체 스킵)를 대체.
- CI에서 Next.js 앱 직접 빌드 후 localhost:3000 서버 기동
- non-@auth 테스트: 항상 로컬 서버 대상 실행 (스테이징 불필요)
- @auth 테스트: 스테이징 가용 시에만 실행, 불가 시 경고 후 스킵

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
study-platform-client-dev Ready Ready Preview, Comment May 25, 2026 2:08am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

마이페이지의 1:1 문의/결제/포스트 관리를 API v6으로 전환하고, Builder Feed와 QnA에 localStorage 기반 임시저장 기능을 추가했습니다. 결제 환불·취소 모달과 알림 토글을 개선하고, CI Playwright E2E를 로컬 기반과 스테이징 조건으로 분리했으며 여러 UI 스타일 토큰을 통합했습니다.

Changes

마이페이지 기능 확장 및 스타일 통합

Layer / File(s) Summary
API v6 클라이언트 및 기반 타입 정의
src/api/client/axios.ts, src/hooks/queries/my-inquiry/inquiry-api.ts, src/types/api/course.types.ts, src/types/schemas/inquiry.schema.ts
v6 전용 axios 인스턴스 추가 및 로거/인터셉터 구성, 1:1 문의 훅·타입, 코스 결제·환불 타입과 빌더 피드 관리 응답 타입, 문의 zod 스키마를 추가했습니다.
1:1 문의 작성 및 목록 조회
src/app/(service)/(my)/my-inquiry/write/page.tsx, src/app/(service)/(my)/my-inquiry/page.tsx
카테고리/내용 기반 작성 폼으로 전환, localStorage 임시저장·복원, useSaveDraft/useCreate 훅 연동, 목록의 로딩/에러/빈 상태 분기 및 상세 비동기 조회 패널을 구현했습니다.
결제 환불 요청 및 취소 모달
src/app/(service)/(my)/class-payment-management/page.tsx, src/components/payment/modals/class-refund-request-modal.tsx, src/components/payment/modals/class-cancel-payment-modal.tsx, src/hooks/queries/course/course-api.ts
가상계좌 상세 조회 연동, 환불 사유 선택 기반 모달 추가, 결제 취소 확인 모달 간소화, 결제 상태 배지/Badge 사용, 관련 react-query 훅 추가·무효화 처리 변경을 적용했습니다.
Builder Feed 임시저장 기능
src/app/(landing)/class/[slug]/(learning)/feed/write/page.tsx, src/types/api/course.types.ts
FEED_NOTICE 안내 추가, localStorage 기반 draft load/save, draftFeedIdRef 도입, handleSaveDraft/handleSubmit 분기와 DRAFT/PUBLISHED 처리 및 CTA 변경을 적용했습니다.
QnA 임시저장 및 유의사항 추가
src/app/(landing)/class/[slug]/(learning)/qa/write/page.tsx
QNA_NOTICE 안내 추가, localStorage 기반 draft load/save, 임시저장 버튼과 등록 성공 시 draft 제거 로직을 도입하고 취소 모달을 제거했습니다.
알림 토글 및 모달 UI 개선
src/app/(service)/(my)/my-class/page.tsx, src/app/(service)/(my)/my-class/_components/disable-notification-modal.tsx, src/app/(service)/(my)/my-class/_components/learning-notification-modal.tsx
ToggleSwitch 기반 토글로 전환, DisableNotificationModal 추가, LearningNotificationModal에 onSuccess 콜백 추가, 로컬 비활성화(localStorage) 추적 및 알림 카드 스타일 토큰 업데이트를 적용했습니다.
마이 포스트 관리 API 기반 재구성
src/app/(service)/(my)/my-posts/page.tsx, src/hooks/queries/course/course-api.ts
useGetMyBuilderFeedManagement 훅 도입으로 PUBLISHED/DRAFT 분리, FilterDropdown·FeedCard·DraftFeedCard 추가, 삭제 토스트 연동 및 통계 UI 갱신을 구현했습니다.
스타일 토큰 통합 및 UI 미세 조정
여러 파일(src/app/..., src/components/...)
다수 컴포넌트에서 border/text/bg 토큰과 클래스(예: border-primary-500 → border-border-brand 등)를 브랜드 토큰으로 통합하고 모달/배너/사이드바 라벨 및 버튼 스타일을 조정했습니다.
CI/E2E 테스트 전략 변경
.github/workflows/ci.yml, e2e/class/builder-feed.spec.ts
로컬 서버 기반 비인증 E2E 항상 실행(로컬:3000), 스테이징 연결성 검사 후 인증 E2E 조건부 실행(STAGING_DOWN 플래그), NEXT_PUBLIC_API_BASE_URL 주입, 로컬 서버 폴링 및 E2E 테스트 동기화 방식을 특정 API 응답 대기로 변경했습니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 초안은 숨 쉬고, 저장은 춤추네
v6로 길을 내고, 문의와 환불 다듬어
로컬에서 시험하고, 스테이징은 살피며
토큰은 고쳐 입히고, 화면은 다듬었네
작은 당근 한 입 축하의 춤을!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 '마이페이지 기능 구현 및 디자인 개선'으로, 변경 사항의 주요 범위와 목적을 명확하게 요약하고 있습니다. 마이페이지 관련 기능 추가 및 UI 개선이라는 핵심 변경이 정확히 반영되어 있습니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cherry/fix-myPage-to-main

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/app/(service)/(my)/my-inquiry/write/page.tsx (1)

157-168: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

체크박스 색상에 하드코딩 Tailwind 색상(accent-rose-500)을 사용하고 있습니다.

앱 라우트 파일에서는 프로젝트 토큰 기반 클래스만 사용해야 하므로 토큰 클래스로 치환해 주세요.

As per coding guidelines src/app/**/*.{tsx,jsx}: "Never use Tailwind base classes ... Always use project custom tokens from global.css."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(service)/(my)/my-inquiry/write/page.tsx around lines 157 - 168, The
checkbox inputs (the input tied to notifyKakao and the earlier email-notify
input) use a hardcoded Tailwind color class "accent-rose-500"; replace that with
the project's token-based class from global.css (e.g., the project's checkbox
accent token such as "accent-primary" or the established token class used
elsewhere in the app) so the inputs use the design token instead of a Tailwind
base class; update both input elements' className values to the token class and
ensure the same token is used consistently for all checkboxes in this component.
src/components/payment/modals/class-cancel-payment-modal.tsx (1)

28-35: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

요청 중 닫기와 성공 콜백 사이 상태 경합을 막아주세요.

요청이 pending인 동안 닫으면, 이후 onSuccess가 도착해 stepcomplete로 다시 바꿀 수 있어 재오픈 시 완료 화면이 먼저 노출될 수 있습니다.

수정 예시
+ import { useEffect, useRef, useState } from 'react';
...
  const [step, setStep] = useState<'form' | 'complete'>('form');
+ const isOpenRef = useRef(open);
+ useEffect(() => {
+   isOpenRef.current = open;
+ }, [open]);

  const handleConfirm = () => {
    cancelPayment(
      { courseId, paymentId },
      {
-       onSuccess: () => setStep('complete'),
+       onSuccess: () => {
+         if (isOpenRef.current) setStep('complete');
+       },
      },
    );
  };

  const handleClose = () => {
+   if (isPending) return;
    onOpenChange(false);
    setStep('form');
  };

Also applies to: 37-40

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/payment/modals/class-cancel-payment-modal.tsx` around lines 28
- 35, The confirm handler can race with modal close: when cancelPayment is
pending and the modal is closed, its onSuccess can later call
setStep('complete') causing the completed view to show on reopen; to fix,
introduce a stable flag/ref (e.g., isOpenRef or isActiveRef) that is set true
while the modal is mounted/open and set false in the modal close/unmount
handler, then change the cancelPayment onSuccess callback in handleConfirm to
check that flag before calling setStep('complete') (or use the mutation promise
and check the flag before setStep); apply the same pattern to the other
cancelPayment call referenced at the 37-40 block so callbacks are ignored when
the modal is closed.
🧹 Nitpick comments (6)
src/app/(landing)/class/[slug]/(learning)/qa/write/page.tsx (1)

95-105: 💤 Low value

handleSaveDraft에서 빈 내용 검증이 누락되었습니다.

handleSubmit에서는 text 내용이 비어있는지 검증하지만(Line 112-115), handleSaveDraft는 레슨 선택만 검증합니다. 사용자가 내용 없이 임시저장할 수 있으며, 이는 의도된 동작일 수도 있지만 피드 작성 페이지의 handleSaveDraft와 일관성이 없습니다.

💡 피드 페이지와 일관된 검증 추가 제안
 function handleSaveDraft() {
   if (!selectedLessonId) {
     showToast('레슨을 선택해주세요.', 'error');
     return;
   }
+  if (!text.replace(/<[^>]*>/g, '').trim()) {
+    showToast('내용을 입력해주세요.', 'error');
+    return;
+  }
   localStorage.setItem(
     `course-qna-draft-${slug}`,
     JSON.stringify({ content: text, lessonId: selectedLessonId }),
   );
   showToast('임시저장되었습니다.');
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(landing)/class/[slug]/(learning)/qa/write/page.tsx around lines 95
- 105, handleSaveDraft currently only checks selectedLessonId and allows saving
when text is empty; update handleSaveDraft to validate that text is non-empty
(same as handleSubmit) before calling localStorage.setItem: if text is empty,
call showToast with the same empty-content error message and return. Keep the
existing localStorage key (`course-qna-draft-${slug}`) and payload shape ({
content: text, lessonId: selectedLessonId }) and ensure you reference the same
variables (handleSaveDraft, selectedLessonId, text, slug) so behavior matches
handleSubmit.
src/app/(service)/(my)/my-class/page.tsx (3)

44-48: 💤 Low value

localStorage 초기 로딩 시 깜빡임 개선 고려

현재 locallyDisabled 상태가 false로 시작한 후 useEffect에서 localStorage를 읽어 업데이트하므로, 실제로 비활성화된 상태였다면 초기 렌더링 시 잠깐 활성화된 UI가 보이다가 곧바로 비활성 UI로 전환되는 깜빡임(FOUC)이 발생할 수 있습니다.

사용자 경험 개선을 위해 mounting 상태를 추적하여 첫 로드 시 로딩 상태를 표시하거나, 또는 useState의 lazy initialization을 활용하는 방법을 고려할 수 있습니다. (단, SSR 환경에서는 여전히 제약이 있으므로 mounting 플래그가 더 안전합니다.)

개선 예시
  const [locallyDisabled, setLocallyDisabled] = useState(false);
+ const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setLocallyDisabled(
      localStorage.getItem(NOTIFICATION_DISABLE_KEY) === 'true',
    );
+   setMounted(true);
  }, []);

+ if (!mounted) {
+   return <div>로딩 중...</div>; // 또는 스켈레톤 UI
+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(service)/(my)/my-class/page.tsx around lines 44 - 48, The initial
render causes a flash because locallyDisabled is initialized to false then
updated inside useEffect; change initialization to read localStorage lazily
(e.g., useState(() => typeof window !== 'undefined' ?
localStorage.getItem(NOTIFICATION_DISABLE_KEY) === 'true' : false)) or, safer
for SSR, add a mounting flag (isMounted state set to true in useEffect) and
render a loading/placeholder until isMounted is true before using
locallyDisabled; update references to useEffect, setLocallyDisabled,
locallyDisabled and NOTIFICATION_DISABLE_KEY accordingly so the UI doesn't
render the wrong state on first paint.

74-86: 💤 Low value

ToggleSwitch className에 cn() 사용 권장

프로젝트 가이드라인에 따르면 모든 className 조합은 cn() 함수를 사용하도록 되어 있습니다. 현재 코드는 정상 동작하지만, 일관성을 위해 cn()을 사용하는 것을 권장합니다.

♻️ 개선 제안
  <ToggleSwitch.Root
    checked={isEnabled}
    onCheckedChange={(checked) => {
      if (checked) {
        setAlarmModalOpen(true);
      } else {
        setDisableModalOpen(true);
      }
    }}
    size="lg"
-   className="bg-border-subtle data-[state=checked]:bg-fill-brand-default-default"
+   className={cn(
+     "bg-border-subtle",
+     "data-[state=checked]:bg-fill-brand-default-default"
+   )}
  />

As per coding guidelines: "Always use cn() function for className composition in React components. Do not use template literal classNames."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(service)/(my)/my-class/page.tsx around lines 74 - 86, The
ToggleSwitch.Root component is using a raw className string instead of the
project's className composer; update the className prop on ToggleSwitch.Root to
call the shared cn() helper (e.g., cn("bg-border-subtle",
"data-[state=checked]:bg-fill-brand-default-default")) so all class composition
follows the guideline; locate the ToggleSwitch.Root usage in page.tsx and
replace the literal className value with a cn(...) call while keeping the
existing classes and other props (checked, onCheckedChange, size) unchanged.

27-64: localStorage 기반 알림 비활성화의 다중 기기 사용성 고려

현재 알림 비활성화 상태를 localStorage에 저장하는 설계는 디바이스별로 독립적으로 관리됩니다. 이는 다음과 같은 특성이 있습니다:

  • 사용자가 여러 기기(PC, 모바일 등)를 사용하는 경우, 한 기기에서 끈 알림이 다른 기기에는 반영되지 않음
  • 브라우저 캐시 삭제 시 설정이 초기화됨
  • 서버 설정(notificationSetting.isEnabled)과 클라이언트 설정이 분리되어 관리됨

사용자가 "모든 기기에서 일괄적으로 알림을 끄고 싶다"는 요구가 있을 경우, 향후 서버 측에 임시 비활성화 플래그를 추가하는 것을 고려할 수 있습니다. 현재 설계가 의도된 것이라면 사용자에게 이 동작을 명확히 안내하는 것도 도움이 될 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(service)/(my)/my-class/page.tsx around lines 27 - 64, The current
per-device disable uses localStorage (NOTIFICATION_DISABLE_KEY) which won't sync
across devices; either (A) persist the disable flag server-side by adding an API
and updating useGetNotificationSetting to return a user-scoped disable flag and
modify handleDisableConfirm and handleAlarmSuccess to call that API (instead of
localStorage) so notificationSetting.isEnabled reflects the server state, or (B)
keep localStorage but update the UI (near the disable modal and in MyClassPage
where isEnabled is computed) to show a clear notice that "Disabling is
device-specific and will not affect other devices or survive browser cache
clears"; choose one and update the functions handleDisableConfirm,
handleAlarmSuccess, and the useGetNotificationSetting integration accordingly.
src/hooks/queries/course/course-api.ts (1)

972-976: ⚡ Quick win

status 파라미터 타입을 리터럴 유니온으로 좁혀 주세요.

현재 status?: string 이라서 잘못된 상태값이 API 파라미터/쿼리키에 들어가도 컴파일 단계에서 걸러지지 않습니다. status?: 'DRAFT' | 'PUBLISHED'(또는 공용 타입)으로 제한하는 편이 안전합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/queries/course/course-api.ts` around lines 972 - 976, The status
parameter on useGetMyBuilderFeedManagement is typed too broadly as string;
narrow it to a literal union (e.g., status?: 'DRAFT' | 'PUBLISHED') or reuse a
shared enum/type if one exists (e.g., FeedStatus) to ensure invalid values are
caught at compile time; update the function signature for
useGetMyBuilderFeedManagement and any referenced types passed into the API/React
Query key so callers and query keys use the new union/type consistently.
src/app/(service)/(my)/my-posts/page.tsx (1)

107-110: ⚡ Quick win

src/app 경로에서 Tailwind base/하드코딩 색상 클래스를 토큰 클래스로 치환해 주세요.

border-rose-500, text-gray-800, bg-white, shadow-md 같은 클래스는 이 경로 규칙과 충돌합니다. 프로젝트 토큰(text-text-*, border-border-*, bg-fill-*, shadow-*)으로 통일하는 게 필요합니다.

As per coding guidelines "src/app/**/*.{tsx,jsx}: Never use Tailwind base classes ... Always use project custom tokens from global.css".

Also applies to: 126-127, 296-297, 313-314, 377-377, 396-396

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(service)/(my)/my-posts/page.tsx around lines 107 - 110, Replace
Tailwind base color/utility classes in the page component className strings with
the project's token classes: where you see `border-rose-500` use the border
token family (`border-border-*`), `text-gray-800` -> `text-text-*`, `bg-white`
-> `bg-fill-*`, and `shadow-md` -> `shadow-*`; update the conditional used
around `activeTab === id` (the class string that chooses `'border-b-2
border-rose-500 text-rose-500'` vs `'font-designer-20r text-gray-800'`) and the
other occurrences referenced (near the other className strings at the noted
ranges) to use `border-border-...`, `text-text-...`, `bg-fill-...`, and
`shadow-...` tokens respectively so all classes under src/app use the project
tokens from global.css.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 178-189: The "Start local server" step currently backgrounds
"NEXT_PUBLIC_API_BASE_URL=... yarn start &" and polls localhost:3000 up to 30
times but does not fail the job if the server never becomes ready; update that
step to detect when the loop times out and explicitly fail (e.g., echo a clear
error like "Local server failed to start" and exit 1) so CI stops on server
startup failure. Target the polling block that uses curl --max-time 5 and the
for i in $(seq 1 30) loop and add a post-loop check that exits non-zero if the
server never became responsive.

In `@src/app/`(landing)/class/[slug]/(learning)/qa/write/page.tsx:
- Around line 265-271: The 임시저장 button is missing a disabled prop while a QnA
create request may be pending; update the button element that calls
handleSaveDraft to include disabled={createQna.isPending} (or
disabled={createQna.isPending || updateQna?.isPending} if an update flow exists)
so it mirrors the feed page behavior and prevents clicks during in-flight
requests.

In `@src/app/`(service)/(my)/class-payment-management/page.tsx:
- Line 265: The thumbnail div in page.tsx uses hardcoded Tailwind color classes
("from-rose-500 to-rose-300") which violates the project token rule; update the
JSX element (the div with className "h-1000 w-1000 flex-shrink-0 rounded-150
bg-gradient-to-br from-rose-500 to-rose-300") to replace those base color
classes with the corresponding project custom token classes defined in
global.css (use the token names your project provides for the gradient start/end
colors so the class becomes bg-gradient-to-br from-<token-name>
to-<token-name>), ensuring you only swap the color classes and keep the
size/shape classes unchanged.

In `@src/app/`(service)/(my)/my-inquiry/page.tsx:
- Around line 18-21: The ANSWER_COMPLETED status object currently hardcodes
Tailwind base classes in its className ('bg-rose-50 text-rose-500 border
border-rose-200'); replace those base classes with the corresponding project
token classes defined in global.css (use the project token for background, text
color and border color) by updating ANSWER_COMPLETED.className so it references
the custom token utility classes instead of 'text-rose-500' and
'border-rose-200'.

In `@src/app/`(service)/(my)/my-inquiry/write/page.tsx:
- Around line 52-54: handleDraft currently calls saveDraft({ id: 0, ... }) which
hardcodes id=0 and can be misinterpreted by the backend; change the flow in
handleDraft/saveDraft so you first call the appropriate "create draft" endpoint
when no oneToOneInquiryId exists (or verify backend contract) and then use the
returned oneToOneInquiryId for subsequent "patch draft" calls instead of always
PATCHing id: 0 (reference saveDraft and handleDraft to implement
create-then-update flow and consume the returned oneToOneInquiryId). Also
replace the hardcoded checkbox className accent-rose-500 with the project-wide
accent token defined in global.css (update the checkbox component/class usage to
use the token name instead of the explicit color token).

In `@src/app/`(service)/(my)/my-page/page.tsx:
- Around line 369-373: The JSX uses forbidden Tailwind base/arbitrary classes:
remove the bg-white in the outer element's className and replace it with the
repo's token-based background utility (e.g., the project background token like
bg-[token-name] or the designated bg utility), and replace the inner div's
arbitrary inset-[12.5%] with a predefined spacing/position token or a global.css
utility (update the class on the <div className="absolute inset-[12.5%]"> and
the outer className that contains bg-white). Ensure you use the repo's
predefined spacing/position tokens and background token utilities consistent
with other src/app/**/*.tsx files.

In `@src/app/`(service)/(my)/my-posts/page.tsx:
- Around line 457-461: The editHref construction uses feed.courseId as a slug
placeholder which can break route contracts; update the data flow to use a real
courseSlug instead: extend the API response/type and the hook that fetches feeds
to include courseSlug (or perform an id→slug lookup/mapping before rendering),
then change editHref (where editHref and feed.courseId/ feed.lessonId/
feed.feedId are referenced) to build URLs with feed.courseSlug (and preserve
lessonId/feedId query) so routes match the [slug] contract.

In `@src/components/payment/modals/class-refund-request-modal.tsx`:
- Around line 44-46: The handleConfirm path uses a bare "as" cast for reason and
silently returns if missing; replace the cast with a runtime guard using the
"in" operator (or explicit typeof/check) to validate required fields before
calling requestRefund in handleConfirm, and when reason is missing show a
user-facing toast error (e.g., via your existing toast API) instead of a silent
return; apply the same pattern to the similar logic around the code referenced
at 132-136 so all optional fields are runtime-validated and produce an error
toast when required data is unavailable.
- Around line 44-57: The onSuccess callback of requestRefund (used in
handleConfirm and the similar block around lines 60-73) unconditionally calls
setStep('complete'), which can overwrite the step after the modal has been
closed and reopened; add a modal-closed guard using a ref (e.g., isClosedRef or
isMountedRef) and set that ref when the modal closes, then in the onSuccess (and
any other request callbacks) check the ref before calling setStep('complete')
and also prevent closing the modal while the request is pending by tracking a
pending flag (e.g., isPendingRef or state) so close handlers and UI close
controls are disabled until request finishes.

---

Outside diff comments:
In `@src/app/`(service)/(my)/my-inquiry/write/page.tsx:
- Around line 157-168: The checkbox inputs (the input tied to notifyKakao and
the earlier email-notify input) use a hardcoded Tailwind color class
"accent-rose-500"; replace that with the project's token-based class from
global.css (e.g., the project's checkbox accent token such as "accent-primary"
or the established token class used elsewhere in the app) so the inputs use the
design token instead of a Tailwind base class; update both input elements'
className values to the token class and ensure the same token is used
consistently for all checkboxes in this component.

In `@src/components/payment/modals/class-cancel-payment-modal.tsx`:
- Around line 28-35: The confirm handler can race with modal close: when
cancelPayment is pending and the modal is closed, its onSuccess can later call
setStep('complete') causing the completed view to show on reopen; to fix,
introduce a stable flag/ref (e.g., isOpenRef or isActiveRef) that is set true
while the modal is mounted/open and set false in the modal close/unmount
handler, then change the cancelPayment onSuccess callback in handleConfirm to
check that flag before calling setStep('complete') (or use the mutation promise
and check the flag before setStep); apply the same pattern to the other
cancelPayment call referenced at the 37-40 block so callbacks are ignored when
the modal is closed.

---

Nitpick comments:
In `@src/app/`(landing)/class/[slug]/(learning)/qa/write/page.tsx:
- Around line 95-105: handleSaveDraft currently only checks selectedLessonId and
allows saving when text is empty; update handleSaveDraft to validate that text
is non-empty (same as handleSubmit) before calling localStorage.setItem: if text
is empty, call showToast with the same empty-content error message and return.
Keep the existing localStorage key (`course-qna-draft-${slug}`) and payload
shape ({ content: text, lessonId: selectedLessonId }) and ensure you reference
the same variables (handleSaveDraft, selectedLessonId, text, slug) so behavior
matches handleSubmit.

In `@src/app/`(service)/(my)/my-class/page.tsx:
- Around line 44-48: The initial render causes a flash because locallyDisabled
is initialized to false then updated inside useEffect; change initialization to
read localStorage lazily (e.g., useState(() => typeof window !== 'undefined' ?
localStorage.getItem(NOTIFICATION_DISABLE_KEY) === 'true' : false)) or, safer
for SSR, add a mounting flag (isMounted state set to true in useEffect) and
render a loading/placeholder until isMounted is true before using
locallyDisabled; update references to useEffect, setLocallyDisabled,
locallyDisabled and NOTIFICATION_DISABLE_KEY accordingly so the UI doesn't
render the wrong state on first paint.
- Around line 74-86: The ToggleSwitch.Root component is using a raw className
string instead of the project's className composer; update the className prop on
ToggleSwitch.Root to call the shared cn() helper (e.g., cn("bg-border-subtle",
"data-[state=checked]:bg-fill-brand-default-default")) so all class composition
follows the guideline; locate the ToggleSwitch.Root usage in page.tsx and
replace the literal className value with a cn(...) call while keeping the
existing classes and other props (checked, onCheckedChange, size) unchanged.
- Around line 27-64: The current per-device disable uses localStorage
(NOTIFICATION_DISABLE_KEY) which won't sync across devices; either (A) persist
the disable flag server-side by adding an API and updating
useGetNotificationSetting to return a user-scoped disable flag and modify
handleDisableConfirm and handleAlarmSuccess to call that API (instead of
localStorage) so notificationSetting.isEnabled reflects the server state, or (B)
keep localStorage but update the UI (near the disable modal and in MyClassPage
where isEnabled is computed) to show a clear notice that "Disabling is
device-specific and will not affect other devices or survive browser cache
clears"; choose one and update the functions handleDisableConfirm,
handleAlarmSuccess, and the useGetNotificationSetting integration accordingly.

In `@src/app/`(service)/(my)/my-posts/page.tsx:
- Around line 107-110: Replace Tailwind base color/utility classes in the page
component className strings with the project's token classes: where you see
`border-rose-500` use the border token family (`border-border-*`),
`text-gray-800` -> `text-text-*`, `bg-white` -> `bg-fill-*`, and `shadow-md` ->
`shadow-*`; update the conditional used around `activeTab === id` (the class
string that chooses `'border-b-2 border-rose-500 text-rose-500'` vs
`'font-designer-20r text-gray-800'`) and the other occurrences referenced (near
the other className strings at the noted ranges) to use `border-border-...`,
`text-text-...`, `bg-fill-...`, and `shadow-...` tokens respectively so all
classes under src/app use the project tokens from global.css.

In `@src/hooks/queries/course/course-api.ts`:
- Around line 972-976: The status parameter on useGetMyBuilderFeedManagement is
typed too broadly as string; narrow it to a literal union (e.g., status?:
'DRAFT' | 'PUBLISHED') or reuse a shared enum/type if one exists (e.g.,
FeedStatus) to ensure invalid values are caught at compile time; update the
function signature for useGetMyBuilderFeedManagement and any referenced types
passed into the API/React Query key so callers and query keys use the new
union/type consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 98c6ea1a-3567-40f6-82f8-5584261b8482

📥 Commits

Reviewing files that changed from the base of the PR and between 1b85cfc and fd261af.

⛔ Files ignored due to path filters (2)
  • public/my-page/discord-icon.png is excluded by !**/*.png
  • public/my-page/feed-icon.svg is excluded by !**/*.svg
📒 Files selected for processing (25)
  • .github/workflows/ci.yml
  • e2e/class/builder-feed.spec.ts
  • src/api/client/axios.ts
  • src/app/(class-lesson)/class/[slug]/lesson/[id]/_components/lesson-qna-submission-modal.tsx
  • src/app/(landing)/class/[slug]/(learning)/feed/write/page.tsx
  • src/app/(landing)/class/[slug]/(learning)/qa/write/page.tsx
  • src/app/(service)/(my)/class-payment-management/page.tsx
  • src/app/(service)/(my)/my-class/_components/disable-notification-modal.tsx
  • src/app/(service)/(my)/my-class/_components/learning-notification-modal.tsx
  • src/app/(service)/(my)/my-class/page.tsx
  • src/app/(service)/(my)/my-inquiry/page.tsx
  • src/app/(service)/(my)/my-inquiry/write/page.tsx
  • src/app/(service)/(my)/my-page/_components/withdrawal-confirm-modal.tsx
  • src/app/(service)/(my)/my-page/page.tsx
  • src/app/(service)/(my)/my-posts/page.tsx
  • src/app/(service)/(my)/payment-management/page.tsx
  • src/components/common/layout/sidebar/my-page-mobile-nav.tsx
  • src/components/common/layout/sidebar/my-page-sidebar.tsx
  • src/components/common/modals/user-profile-modal.tsx
  • src/components/payment/modals/class-cancel-payment-modal.tsx
  • src/components/payment/modals/class-refund-request-modal.tsx
  • src/hooks/queries/course/course-api.ts
  • src/hooks/queries/my-inquiry/inquiry-api.ts
  • src/types/api/course.types.ts
  • src/types/schemas/inquiry.schema.ts

Comment thread .github/workflows/ci.yml
Comment thread src/app/(landing)/class/[slug]/(learning)/qa/write/page.tsx
<div className="border-border-subtle rounded-200 flex gap-300 border p-300">
{/* 썸네일 */}
<div className="h-1000 w-1000 flex-shrink-0 rounded-150 bg-gradient-to-br from-primary-500 to-rose-300" />
<div className="h-1000 w-1000 flex-shrink-0 rounded-150 bg-gradient-to-br from-rose-500 to-rose-300" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

썸네일 배경 그라디언트에 하드코딩 Tailwind 색상을 사용하고 있습니다.

from-rose-500 to-rose-300는 토큰 기반 클래스 규칙에 맞지 않아 프로젝트 토큰으로 교체가 필요합니다.

As per coding guidelines src/app/**/*.{tsx,jsx}: "Never use Tailwind base classes ... Always use project custom tokens from global.css."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(service)/(my)/class-payment-management/page.tsx at line 265, The
thumbnail div in page.tsx uses hardcoded Tailwind color classes ("from-rose-500
to-rose-300") which violates the project token rule; update the JSX element (the
div with className "h-1000 w-1000 flex-shrink-0 rounded-150 bg-gradient-to-br
from-rose-500 to-rose-300") to replace those base color classes with the
corresponding project custom token classes defined in global.css (use the token
names your project provides for the gradient start/end colors so the class
becomes bg-gradient-to-br from-<token-name> to-<token-name>), ensuring you only
swap the color classes and keep the size/shape classes unchanged.

Comment on lines +18 to +21
ANSWER_COMPLETED: {
label: '답변 완료',
className: 'bg-rose-50 text-rose-500 border border-rose-200',
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

상태 배지에 하드코딩 색상 클래스가 포함되어 있습니다.

text-rose-500, border-rose-200 대신 global.css의 프로젝트 토큰 클래스로 교체해 주세요.

As per coding guidelines src/app/**/*.{tsx,jsx}: "Never use Tailwind base classes ... Always use project custom tokens from global.css."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(service)/(my)/my-inquiry/page.tsx around lines 18 - 21, The
ANSWER_COMPLETED status object currently hardcodes Tailwind base classes in its
className ('bg-rose-50 text-rose-500 border border-rose-200'); replace those
base classes with the corresponding project token classes defined in global.css
(use the project token for background, text color and border color) by updating
ANSWER_COMPLETED.className so it references the custom token utility classes
instead of 'text-rose-500' and 'border-rose-200'.

Comment on lines +52 to +54
await saveDraft({
id: 0,
request: {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 목적: draft API 경로 계약과 호출부가 일치하는지 확인
# 기대: {id}/draft가 "기존 초안 ID"를 요구하는 계약인지 확인되면 현재 id=0 고정은 수정 필요

rg -n -C3 "one-to-one-inquiries/.*/draft|one-to-one-inquiries/\\$\\{id\\}/draft|saveDraft\\("

# 호출부에서 id 전달 방식 확인
rg -n -C3 "useSaveDraftOneToOneInquiry|mutateAsync: saveDraft|id:\\s*0"

Repository: code-zero-to-one/study-platform-client

Length of output: 3423


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/app/(service)/(my)/my-inquiry/write/page.tsx"
API="src/hooks/queries/my-inquiry/inquiry-api.ts"

echo "---- page.tsx (lines 1-120) ----"
nl -ba "$FILE" | sed -n '1,120p'

echo "---- page.tsx (lines 120-220) ----"
nl -ba "$FILE" | sed -n '120,220p'

echo "---- inquiry-api.ts (lines 70-150) ----"
nl -ba "$API" | sed -n '70,160p'

Repository: code-zero-to-one/study-platform-client

Length of output: 176


🏁 Script executed:

#!/bin/bash
set -euo pipefail

PAGE="src/app/(service)/(my)/my-inquiry/write/page.tsx"
API="src/hooks/queries/my-inquiry/inquiry-api.ts"

echo "== rg accent-rose-500 in page.tsx =="
rg -n --fixed-string "accent-rose-500" "$PAGE" || true

echo "== rg saveDraftOneToOneInquiry id usage =="
rg -n "useSaveDraftOneToOneInquiry|mypage/one-to-one-inquiries/\\$\\{id\\}/draft|saveDraft\\(" "$API" "$PAGE"

echo "== show page.tsx around saveDraft call =="
START=35; END=90
sed -n "${START},${END}p" "$PAGE" | cat

echo "== show page.tsx around first accent occurrence (±40 lines if found) =="
if rg -n --fixed-string "accent-rose-500" "$PAGE" >/tmp/acc_lines.txt 2>/dev/null; then
  LINE=$(head -n1 /tmp/acc_lines.txt | cut -d: -f1)
  START=$((LINE-40)); if [ $START -lt 1 ]; then START=1; fi
  END=$((LINE+40))
  sed -n "${START},${END}p" "$PAGE" | cat
fi

echo "== show inquiry-api.ts around saveDraft hook =="
# locate line range dynamically
SAVE_LINE=$(rg -n "useSaveDraftOneToOneInquiry" "$API" | head -n1 | cut -d: -f1)
START=$((SAVE_LINE-20)); if [ $START -lt 1 ]; then START=1; fi
END=$((SAVE_LINE+80))
sed -n "${START},${END}p" "$API" | cat

Repository: code-zero-to-one/study-platform-client

Length of output: 3449


🏁 Script executed:

#!/bin/bash
set -euo pipefail

PAGE="src/app/(service)/(my)/my-inquiry/write/page.tsx"
API="src/hooks/queries/my-inquiry/inquiry-api.ts"

echo "== rg accent-rose-500 in page.tsx =="
rg -n --fixed-string "accent-rose-500" "$PAGE" || true

echo "== rg saveDraftOneToOneInquiry id usage =="
rg -n "useSaveDraftOneToOneInquiry|mypage/one-to-one-inquiries/\\$\\{id\\}/draft|saveDraft\\(" "$API" "$PAGE"

echo "== show page.tsx around saveDraft call =="
START=35; END=90
sed -n "${START},${END}p" "$PAGE" | cat

echo "== show page.tsx around first accent occurrence (±40 lines if found) =="
if rg -n --fixed-string "accent-rose-500" "$PAGE" >/tmp/acc_lines.txt 2>/dev/null; then
  LINE=$(head -n1 /tmp/acc_lines.txt | cut -d: -f1)
  START=$((LINE-40)); if [ $START -lt 1 ]; then START=1; fi
  END=$((LINE+40))
  sed -n "${START},${END}p" "$PAGE" | cat
fi

echo "== show inquiry-api.ts around saveDraft hook =="
SAVE_LINE=$(rg -n "useSaveDraftOneToOneInquiry" "$API" | head -n1 | cut -d: -f1)
START=$((SAVE_LINE-20)); if [ $START -lt 1 ]; then START=1; fi
END=$((SAVE_LINE+80))
sed -n "${START},${END}p" "$API" | cat

Repository: code-zero-to-one/study-platform-client

Length of output: 3449


🏁 Script executed:

#!/bin/bash
set -euo pipefail

PAGE="src/app/(service)/(my)/my-inquiry/write/page.tsx"
API="src/hooks/queries/my-inquiry/inquiry-api.ts"

echo "== page.tsx: hasDraft/params/기존 inquiryId 추출부 확인 (1-120) =="
sed -n '1,140p' "$PAGE" | cat

echo "== page.tsx: draft 관련 상태/props/데이터 매핑 확인 (140-240) =="
sed -n '140,240p' "$PAGE" | cat

echo "== inquiry-api.ts: DraftOneToOneInquiryRequest 타입/주변(1-120) =="
sed -n '1,140p' "$API" | cat

echo "== inquiry-api.ts: CreateOneToOneInquiryRequest 및 기타 드래프트 관련 타입(140-240) =="
sed -n '140,260p' "$API" | cat

echo "== page.tsx: accent- 관련 토큰 혹시 다른 형태 존재하는지(rose/gray/red 등) =="
rg -n "accent-|rose|rose-" "$PAGE" || true

Repository: code-zero-to-one/study-platform-client

Length of output: 11018


🏁 Script executed:

#!/bin/bash
set -euo pipefail

PAGE="src/app/(service)/(my)/my-inquiry/write/page.tsx"
API="src/hooks/queries/my-inquiry/inquiry-api.ts"

echo "== page.tsx: hasDraft/params/기존 inquiryId 추출부 확인 (1-140) =="
sed -n '1,140p' "$PAGE" | cat

echo "== page.tsx: draft 관련 상태/props/데이터 매핑 확인 (140-240) =="
sed -n '140,240p' "$PAGE" | cat

echo "== inquiry-api.ts: DraftOneToOneInquiryRequest 타입/주변(1-140) =="
sed -n '1,140p' "$API" | cat

echo "== inquiry-api.ts: CreateOneToOneInquiryRequest 및 기타 드래프트 관련 타입(140-260) =="
sed -n '140,260p' "$API" | cat

echo "== page.tsx: accent- 관련 토큰 혹시 다른 형태 존재하는지(rose/gray/red 등) =="
rg -n "accent-|rose|rose-" "$PAGE" || true

Repository: code-zero-to-one/study-platform-client

Length of output: 11018


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "useSaveDraftOneToOneInquiry|one-to-one-inquiries/\\$\\{id\\}/draft|saveDraft\\(\\{" . \
  --glob '!**/node_modules/**' --glob '!**/dist/**' || true

rg -n "id:\\s*0" src --glob '!**/node_modules/**' || true

Repository: code-zero-to-one/study-platform-client

Length of output: 613


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "useSaveDraftOneToOneInquiry|one-to-one-inquiries/\\$\\{id\\}/draft|saveDraft\\(\\{" . \
  --glob '!**/node_modules/**' --glob '!**/dist/**' || true

rg -n "id:\\s*0" src --glob '!**/node_modules/**' || true

Repository: code-zero-to-one/study-platform-client

Length of output: 613


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "one-to-one-inquiries/.*/draft|/draft', request|/draft\`|/draft\)" src || true
rg -n "id:\s*0" src || true
rg -n "draft" src/hooks src/app || true

Repository: code-zero-to-one/study-platform-client

Length of output: 3933


임시저장 id: 0 고정과 체크박스 accent-rose-500 사용 수정 필요

  • handleDraft에서 saveDraft({ id: 0, ... })mypage/one-to-one-inquiries/${id}/draft PATCH를 호출 중인데, 서버가 이 id를 기존 oneToOneInquiryId로 해석한다면 초안 대상 식별 실패(404/검증 오류) 가능 → 백엔드에서 id=0을 신규 draft 생성으로 처리하는지 계약 확인 후, 필요 시 초안 생성 → 반환된 oneToOneInquiryId로 “초안 수정” 흐름으로 분리.
  • 체크박스에 className="... accent-rose-500"가 실제로 사용 중이며, 하드코딩된 색상 토큰이라 global.css의 프로젝트 accent 토큰으로 교체 필요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(service)/(my)/my-inquiry/write/page.tsx around lines 52 - 54,
handleDraft currently calls saveDraft({ id: 0, ... }) which hardcodes id=0 and
can be misinterpreted by the backend; change the flow in handleDraft/saveDraft
so you first call the appropriate "create draft" endpoint when no
oneToOneInquiryId exists (or verify backend contract) and then use the returned
oneToOneInquiryId for subsequent "patch draft" calls instead of always PATCHing
id: 0 (reference saveDraft and handleDraft to implement create-then-update flow
and consume the returned oneToOneInquiryId). Also replace the hardcoded checkbox
className accent-rose-500 with the project-wide accent token defined in
global.css (update the checkbox component/class usage to use the token name
instead of the explicit color token).

Comment on lines +369 to +373
className="border-border-brand rounded-200 flex flex-1 items-center justify-center gap-175 overflow-hidden border bg-white py-250 transition-colors hover:bg-gray-50"
>
<div className="bg-primary-100 flex size-1000 shrink-0 items-center justify-center rounded-full">
<span className="font-designer-16b text-primary-600">B</span>
<div className="relative h-675 w-675 shrink-0 overflow-hidden">
<div className="absolute inset-[12.5%]">
<Image
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

앱 라우트 스타일 가이드 위반 클래스 제거 필요

bg-white(베이스 클래스)와 inset-[12.5%](arbitrary value)는 이 리포의 src/app/**/*.tsx 규칙에 맞지 않습니다. 토큰 기반 배경 클래스와 사전정의된 spacing/position 토큰(또는 global.css 유틸)으로 교체해 주세요.

As per coding guidelines, src/app/**/*.{tsx,jsx}에서는 Tailwind base class와 arbitrary value 사용이 금지되어 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(service)/(my)/my-page/page.tsx around lines 369 - 373, The JSX uses
forbidden Tailwind base/arbitrary classes: remove the bg-white in the outer
element's className and replace it with the repo's token-based background
utility (e.g., the project background token like bg-[token-name] or the
designated bg utility), and replace the inner div's arbitrary inset-[12.5%] with
a predefined spacing/position token or a global.css utility (update the class on
the <div className="absolute inset-[12.5%]"> and the outer className that
contains bg-white). Ensure you use the repo's predefined spacing/position tokens
and background token utilities consistent with other src/app/**/*.tsx files.

Comment on lines +457 to +461
// TODO: courseSlug not in API response — using courseId as slug placeholder
const editHref =
feed.lessonId !== null
? `/class/${feed.courseId}/lesson/${feed.lessonId}?feedId=${feed.feedId}`
: `/class/${feed.courseId}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

임시저장 수정 링크가 라우트 계약(slug)과 불일치할 가능성이 큽니다.

[slug] 자리에 courseId를 placeholder로 쓰고 있어 수정 페이지 이동이 깨질 수 있습니다. courseSlug를 API 응답/타입에 포함하거나, id→slug 매핑을 선행한 뒤 링크를 생성해야 합니다.

원하시면 courseSlug 계약 반영(타입/훅/링크 생성부) 기준으로 후속 이슈 템플릿을 바로 정리해드릴게요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(service)/(my)/my-posts/page.tsx around lines 457 - 461, The
editHref construction uses feed.courseId as a slug placeholder which can break
route contracts; update the data flow to use a real courseSlug instead: extend
the API response/type and the hook that fetches feeds to include courseSlug (or
perform an id→slug lookup/mapping before rendering), then change editHref (where
editHref and feed.courseId/ feed.lessonId/ feed.feedId are referenced) to build
URLs with feed.courseSlug (and preserve lessonId/feedId query) so routes match
the [slug] contract.

Comment on lines +44 to +46
const handleConfirm = () => {
if (!reason) return;
requestRefund(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

환불 사유는 런타임 가드로 검증하고, 누락 시 토스트를 보여주세요.

현재는 bare as 단언으로 reason을 설정하고, reason이 없으면 조용히 return 합니다. 잘못된 값 유입/무응답 UX를 함께 만들 수 있습니다.

수정 예시
+ import { useToastStore } from '`@/stores/use-toast-store`';
...
  const [detail, setDetail] = useState('');
+ const showToast = useToastStore((state) => state.showToast);

  const handleConfirm = () => {
-   if (!reason) return;
+   if (!reason) {
+     showToast('환불 사유를 선택해 주세요.', 'error');
+     return;
+   }
    requestRefund(
...
                <SingleDropdown
                  options={REFUND_REASONS}
                  value={reason}
-                 onChange={(v) => setReason(v as CourseRefundReasonCode)}
+                 onChange={(v) => {
+                   const nextReason =
+                     typeof v === 'string' &&
+                     REFUND_REASONS.some((r) => r.value === v)
+                       ? (v as CourseRefundReasonCode)
+                       : undefined;
+                   if (!nextReason) {
+                     showToast('유효하지 않은 환불 사유예요.', 'error');
+                     return;
+                   }
+                   setReason(nextReason);
+                 }}
                  placeholder="사유를 선택해 주세요."
                  size="l"
                />

As per coding guidelines "src/components/**/*.{ts,tsx}: Use the in guard operator with fallback ... Never use bare as assertions..." and "Guard handlers for optional fields ... show a toast error message if required data is unavailable".

Also applies to: 132-136

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/payment/modals/class-refund-request-modal.tsx` around lines 44
- 46, The handleConfirm path uses a bare "as" cast for reason and silently
returns if missing; replace the cast with a runtime guard using the "in"
operator (or explicit typeof/check) to validate required fields before calling
requestRefund in handleConfirm, and when reason is missing show a user-facing
toast error (e.g., via your existing toast API) instead of a silent return;
apply the same pattern to the similar logic around the code referenced at
132-136 so all optional fields are runtime-validated and produce an error toast
when required data is unavailable.

Comment thread src/components/payment/modals/class-refund-request-modal.tsx
HA-SEUNG-JEONG and others added 2 commits May 25, 2026 11:02
…틸 export

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ent HTML 스트립 공용화

- qa/write: 임시저장 버튼 createQna.isPending 중 disabled 처리
- class-refund: closedRef로 모달 닫힘 후 onSuccess step 덮어씀 방지
- my-posts: feedContent.replace() → stripHtml() 공용 유틸로 교체

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
src/components/payment/modals/class-refund-request-modal.tsx (2)

49-51: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

환불 사유 검증이 여전히 런타임 가드 없이 처리되고, 누락 시 무응답입니다.

reason 누락 시 조용히 반환하고(Line 50), 드롭다운 값도 bare as로 단언하고 있어 잘못된 값 유입 시 UX/안정성이 떨어집니다. 가드 + 토스트 처리로 바꿔 주세요.

수정 예시
 import { useRequestCourseRefund } from '`@/hooks/queries/course/course-api`';
+import { useToastStore } from '`@/stores/use-toast-store`';
 import type { CourseRefundReasonCode } from '`@/types/api/course.types`';

+const REFUND_REASON_GUARD: Record<CourseRefundReasonCode, true> = {
+  REPAYMENT_AFTER_METHOD_CHANGE: true,
+  DUPLICATE_PAYMENT: true,
+  CHANGE_OF_MIND: true,
+  UNSATISFIED_CONTENT: true,
+  TECHNICAL_ISSUE: true,
+  OTHER: true,
+};
+
+const isCourseRefundReasonCode = (
+  value: unknown,
+): value is CourseRefundReasonCode =>
+  typeof value === 'string' && value in REFUND_REASON_GUARD;
+
 export default function ClassRefundRequestModal({
@@
 }: ClassRefundRequestModalProps) {
+  const showToast = useToastStore((state) => state.showToast);
@@
   const handleConfirm = () => {
-    if (!reason) return;
+    if (!reason) {
+      showToast('환불 사유를 선택해 주세요.', 'error');
+      return;
+    }
@@
                 <SingleDropdown
                   options={REFUND_REASONS}
                   value={reason}
-                  onChange={(v) => setReason(v as CourseRefundReasonCode)}
+                  onChange={(v) => {
+                    if (!isCourseRefundReasonCode(v)) {
+                      showToast('유효하지 않은 환불 사유예요.', 'error');
+                      return;
+                    }
+                    setReason(v);
+                  }}
                   placeholder="사유를 선택해 주세요."
                   size="l"
                 />

As per coding guidelines "src/components/**/*.{ts,tsx}: Guard handlers for optional fields ... show a toast error message if required data is unavailable" and "Use the in guard operator with fallback ... Never use bare as assertions without runtime guards".

Also applies to: 140-144

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/payment/modals/class-refund-request-modal.tsx` around lines 49
- 51, handleConfirm currently silently returns when reason is missing and uses
bare "as" assertions for dropdown values; change it to runtime-guard the reason
and dropdown value before calling requestRefund: inside handleConfirm check that
reason is present using an "in" or truthy guard and if missing call the toast
error helper (show a user-facing error) and return; for the dropdown value
replace bare "as" assertions with a guarded type check (using "in" or a
validated fallback) so you only pass a well-formed value to requestRefund; apply
the same guard+toast pattern to the other handler around the 140-144 section.

45-47: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

닫았다가 재오픈하면 이전 요청의 성공 콜백이 새 모달 상태를 덮어쓸 수 있습니다.

closedRefopen=true에서 즉시 false로 되돌려서, 이전 in-flight 요청이 늦게 성공하면 새 세션에서 setStep('complete')가 다시 실행될 수 있습니다. 요청 단위 식별자(ref)를 같이 써서 stale 콜백을 무효화해 주세요.

수정 예시
   const { mutate: requestRefund, isPending } = useRequestCourseRefund();
   const closedRef = useRef(false);
+  const requestSeqRef = useRef(0);

   useEffect(() => {
-    if (open) closedRef.current = false;
+    if (!open) {
+      closedRef.current = true;
+      requestSeqRef.current += 1; // close 시 이전 요청 콜백 무효화
+    }
   }, [open]);

   const handleConfirm = () => {
     if (!reason) return;
+    const requestSeq = ++requestSeqRef.current;
+    closedRef.current = false;
     requestRefund(
       {
         paymentId,
@@
       {
         onSuccess: () => {
-          if (!closedRef.current) setStep('complete');
+          if (closedRef.current || requestSeq !== requestSeqRef.current) return;
+          setStep('complete');
         },
       },
     );
   };

   const handleClose = () => {
     closedRef.current = true;
+    requestSeqRef.current += 1;
     onOpenChange(false);
     setStep('form');
     setReason(undefined);
     setDetail('');
   };

   const handleCompleteClose = () => {
+    closedRef.current = true;
+    requestSeqRef.current += 1;
     onOpenChange(false);
     setStep('form');
     setReason(undefined);
     setDetail('');
     router.push('/class-payment-management');
   };

Also applies to: 60-62, 68-73

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/payment/modals/class-refund-request-modal.tsx` around lines 45
- 47, The current useEffect resets closedRef on open which allows a previous
in-flight request's success callback to affect a newly opened modal; add a
per-request identifier ref (e.g., requestIdRef) that's incremented whenever the
modal opens (inside the same useEffect or open handling), capture the current id
when starting each async request, and in every success/finally callback (where
setStep('complete') or other state updates occur) check that the captured id
still matches requestIdRef.current before calling setStep or touching state;
apply the same pattern to the other request sites mentioned (around the blocks
corresponding to lines ~60-62 and ~68-73) so stale callbacks are ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/components/payment/modals/class-refund-request-modal.tsx`:
- Around line 49-51: handleConfirm currently silently returns when reason is
missing and uses bare "as" assertions for dropdown values; change it to
runtime-guard the reason and dropdown value before calling requestRefund: inside
handleConfirm check that reason is present using an "in" or truthy guard and if
missing call the toast error helper (show a user-facing error) and return; for
the dropdown value replace bare "as" assertions with a guarded type check (using
"in" or a validated fallback) so you only pass a well-formed value to
requestRefund; apply the same guard+toast pattern to the other handler around
the 140-144 section.
- Around line 45-47: The current useEffect resets closedRef on open which allows
a previous in-flight request's success callback to affect a newly opened modal;
add a per-request identifier ref (e.g., requestIdRef) that's incremented
whenever the modal opens (inside the same useEffect or open handling), capture
the current id when starting each async request, and in every success/finally
callback (where setStep('complete') or other state updates occur) check that the
captured id still matches requestIdRef.current before calling setStep or
touching state; apply the same pattern to the other request sites mentioned
(around the blocks corresponding to lines ~60-62 and ~68-73) so stale callbacks
are ignored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b79b6096-6e1c-42f4-ba94-b3012c2386ed

📥 Commits

Reviewing files that changed from the base of the PR and between fd261af and d440b0e.

📒 Files selected for processing (5)
  • .github/workflows/ci.yml
  • src/app/(landing)/class/[slug]/(learning)/qa/write/page.tsx
  • src/app/(service)/(my)/my-posts/page.tsx
  • src/components/payment/modals/class-refund-request-modal.tsx
  • src/utils/markdown-content-text.ts

@HA-SEUNG-JEONG HA-SEUNG-JEONG merged commit 538e149 into main May 25, 2026
12 checks passed
@HA-SEUNG-JEONG HA-SEUNG-JEONG deleted the cherry/fix-myPage-to-main branch May 25, 2026 02:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:minor Minor production release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant